package me.ycy.android.shuffle.synchronizer

import android.app.Activity
import android.database.ContentObserver
import android.os.{ Bundle, IBinder }
import android.util.Log
import android.app.Service
import android.content.{ Context, Intent, ServiceConnection,
                        ComponentName, ContentUris,
                        BroadcastReceiver }
import android.preference.PreferenceManager
import android.widget._

import org.ektorp._
import org.ektorp.http.AndroidHttpClient
import org.ektorp.impl.{ StdCouchDbConnector, StdCouchDbInstance }

import me.ycy.android.couchbase.server.ICouchbaseService
import me.ycy.android.helper.ContentValuesWrapper

import java.util.{ Date, Timer, TimerTask }

class BootReceiver extends BroadcastReceiver {
  val Tag = "ShuffleSync"

  def onReceive(ctx: Context, i: Intent) = {
    Log.i(Tag, "service start on boot.")
    val ac = ctx.getApplicationContext
    val settings = PreferenceManager.getDefaultSharedPreferences(ac)
    if (settings.getBoolean("serviceEnabled", false)) {
      ac.startService(new Intent(ac, classOf[ShuffleSynchronizer]))
    }
    this
  }
}

class ShuffleSynchronizer extends SyncImpl with Logging

class SyncImpl extends Service {
  lazy val self = this
  lazy val cr = this.getApplicationContext.getContentResolver
  val Tag = "ShuffleSync"
  lazy val worker = new SyncWorker(this)
  // ------------------ couchbase ----------------------
  val DbName = "shuffle"
  var timer: Timer = null

  class RebindTask extends TimerTask {
    def run = {
      if (db == null)
        bindService(new Intent(classOf[ICouchbaseService].getName),
                    conn, Context.BIND_AUTO_CREATE)
    }
  }

  val contentLock = new Object

  var db: CouchDbConnector = null
  val conn: ServiceConnection = new ServiceConnection() {
    def onServiceConnected(className: ComponentName, service: IBinder): Unit = {
      val couch = ICouchbaseService.Stub.asInterface(service)
      new Thread {
        override def run = {
          while(couch.getPort == -1) {
            // sleep 1sec
            Thread.sleep(1000)
          }
          initDb(couch.getHost, couch.getPort)
        }
      }.start
    }

    def onServiceDisconnected(className: ComponentName): Unit = {
      Log.e(Tag, "Couchbase service has unexpectedly disconnected")
      Log.v(Tag, "try to rebind service in 10 sec.")
      timer.schedule(new RebindTask, 10 * 1000)
      db = null
    }
  }

  def initDb(host: String, port: Int) = {
    // TODO, https scheme
    Log.i(Tag, "try to connect to remote server " + host + ":" + port)
    try {
      val httpClient = new AndroidHttpClient.Builder().
        host(host).port(port).build
      val dbInstance = new StdCouchDbInstance(httpClient)
      db = new StdCouchDbConnector(DbName, dbInstance)
      db.createDatabaseIfNotExists()
    }
    catch {
      case e =>
        Toast.makeText(getApplicationContext(), "Error: " + e.getMessage,
                       Toast.LENGTH_LONG).show()
    }
  }

  // -------------------- local config ----------------------
  object cf {
    def settings = PreferenceManager.
      getDefaultSharedPreferences(self.getApplicationContext)

    val Seq = "seq"
    val Timestamp = "timestamp"

    def seq: Long = {
      // couchdb start from seq 0
      settings.getLong(Seq, 0)
    }

    def seq_=(s: Long) = {
      val e = settings.edit
      e.putLong(Seq, s)
      e.commit
    }

    def timestamp: Date = {
      // shuffle default context modified time is 1970-01-01 00:00:00
      new Date(settings.getLong(Timestamp, -1))
    }

    def timestamp_=(t: Date) = {
      val e = settings.edit
      e.putLong(Timestamp, t.getTime)
      e.commit
    }

    def port: Int = {
      try {
        settings.getString("port", "5984").toInt
      }
      catch {
        case _ => 5984
      }
    }
    def host: String = settings.getString("host", "127.0.0.1")
    def useLocal: Boolean = settings.getBoolean("useLocal", false)
  }

  // --------------------- local change ---------------------
  object observer extends ContentObserver(null) {
    val Tag = "ShuffleObserver"

    override def onChange(selfChange: Boolean) = {
      super.onChange(selfChange)
      Log.v(Tag, "self change " + selfChange)

      onLocalChange
    }
  }

  def onLocalChange(): Unit = onLocalChange(cf.timestamp)
  def onLocalChange(from: Date): Unit = contentLock.synchronized {
    import Helper._
    val now = new Date
    var items: List[ektorp.ShuffleItem] = List()

    Log.d(Tag, "================= begin dump ===================")

    Log.d(Tag, "from =>" + from)

    var lastModified = from

    def getLastModified(provider: Shuffle.Provider, category: String) = {
      withCursor(cr.query(provider.uri,
                        Array("MAX(" + provider.ModifiedDate + ") as x"),
                          null, null, null)) { cur => {
        if (cur.moveToNext) {
          val l = cur.getLong(0)
          Log.d(Tag, category + "last modified: " + new Date(l))
          lastModified = new Date(math.max(l, lastModified.getTime))
        }
      }}
    }

    getLastModified(Shuffle.Tasks, "task")
    getLastModified(Shuffle.Projects, "project")
    getLastModified(Shuffle.Contexts, "context")

    Log.d(Tag, "to   =>" + lastModified)
    Log.d(Tag, "now  =>" + now)

    if (lastModified.after(now)) {
      Log.e(Tag, "Something changed in the future:\n"+
            "lastModified => " + lastModified  + "\n" +
            "now          => " + now)
      lastModified = now
    }


    // This function also update tracks id.
    def getModifiedItems(provider: Shuffle.Provider) = {
      val sel = "(? < " + provider.ModifiedDate + ") AND (" +
        provider.ModifiedDate + " <= ?)"
      val selArgs = Array(from.getTime.toString, lastModified.getTime.toString)
      val order = provider.ModifiedDate + " ASC"

      withCursor(cr.query(provider.uri, provider.projection,
                          sel, selArgs, order)) { cur => {
        while (cur.moveToNext) {
          val row = cursorExtract(cur, provider.projection, provider.types)
          val item = provider.toItem(row)
          // need update remote id if necessary
          // this will cause content observer callback
          // so make this function synchronized.
          if (item.id == 0) {
            val rand: Long = ((math.random*0x7fffffffL).toLong << 32) |
              (math.random*0xffffffffL).toLong
            val lId = row(provider.Id).as[Long] // local _ID
            // special treat six build in context
            Log.d(Tag, "generate remote id for " + provider.uri + "/" + lId)
            val rId = if ((provider == Shuffle.Contexts) && (lId <= 6)) lId else rand
            val w = new ContentValuesWrapper
            w.putLong(provider.TracksId, rId)
            cr.update(ContentUris.withAppendedId(provider.uri,
                                                 row(provider.Id).as[Long]),
                      w.android, null, null)
            item.id = rId
          }

          // foreign key translation
          item match {
            case i: ektorp.Project => if (i.defaultContextId != 0) {
              val uri = ContentUris.withAppendedId(Shuffle.Contexts.uri,
                                                   i.defaultContextId)
              i.defaultContextId = withCursor(cr.query(uri, Array(provider.TracksId),
                                                       null, null, null)) { c1 => {
                if (c1.moveToFirst) {
                  c1.getLong(0)
                }
                else { // can't find remote id, using zero
                  Log.w(Tag, "Can't find defaultContextId rId for project " + i.name)
                  0
                }
              }}
            }
            case i: ektorp.Task => {
              if (i.contextId != 0) {
                val uri = ContentUris.withAppendedId(Shuffle.Contexts.uri,
                                                     i.contextId)
                i.contextId = withCursor(cr.query(uri, Array(provider.TracksId),
                                             null, null, null)) { c1 => {
                  if (c1.moveToFirst) {
                    c1.getLong(0)
                  }
                  else { // can't find remote id, using zero
                    Log.w(Tag, "Can't find contextId rId for task " + i.description)
                    0
                  }
                }}
              }

              if (i.projectId != 0) {
                val uri = ContentUris.withAppendedId(Shuffle.Projects.uri,
                                                     i.projectId)
                i.projectId = withCursor(cr.query(uri, Array(provider.TracksId),
                                                  null, null, null)) { c1 => {
                  if (c1.moveToFirst) {
                    c1.getLong(0)
                  }
                  else { // can't find remote id, using zero
                    Log.w(Tag, "Can't find projectId rId for task " + i.description)
                    0
                  }
                }}
              }
            }
            case _ =>
          }

          Log.d(Tag, "cur [" + cur.getPosition + "] => " + item)
          items ::= item
        }
      }}
    }

    // Order is important
    // 1. context has no FK to project and task
    // 2. project has FK to context: defaultContextId
    // 3. task has FK to both context and project: contextId & projectId
    getModifiedItems(Shuffle.Contexts)
    getModifiedItems(Shuffle.Projects)
    getModifiedItems(Shuffle.Tasks)

    // sort and put to task queue
    items.sortBy(_.modifiedDate.getTime).foreach { item => {
      val c = LocalChange(item, item.modifiedDate)
      worker.put(c)
    }}

    Log.d(Tag, "================== end dump ====================")
  }

  // ---------------------- remote change ---------------------------
  import org.ektorp.changes._

  def onRemoteChange(change: DocumentChange) = {
    Log.d(Tag, "remote change: " + change)
    val rId = change.getId
    val deleted = change.isDeleted()
    val seq = change.getSequence
    if (deleted) {
      try {
        worker.put(RemoteDelete(java.lang.Long.parseLong(rId, 16), seq))
      }
      catch {
        case e => Log.i(Tag, "remote document '" + rId +
                        "' format error, maybe is not shuffle item")
      }
    }
    else {
      try {
        val item = db.get(classOf[ektorp.ShuffleItem], rId)
        worker.put(RemoteChange(item, seq))
      }
      catch {
        // TODO: should only include exception after get http response.
        case e: DbAccessException => {
          Log.i(Tag, "remote document '" + rId +
                  "' format error, maybe is not shuffle item")
        }
        case e => throw(e)
      }
    }
  }

  val remoteMonitorThread = new Thread {
    var running = true
    override def run = {
      while (running) {
        // wait db ready
        try {
          while (db == null) {
            Thread.sleep(1000)
          }
        }
        catch {
          case _: InterruptedException =>
        }

        // wait changes
        val cmd = new ChangesCommand.Builder().continuous(true).
        since(cf.seq).build()
        var feed: ChangesFeed = null
        try {
          feed = db.changesFeed(cmd) // shoud be in try block
          while (true) {
            onRemoteChange(feed.next)
          }
        }
        catch {
          case e: DocumentNotFoundException => {
            Log.d(Tag, "create db " + DbName)
            db.createDatabaseIfNotExists()
          }
          case _: InterruptedException =>
            Log.d(Tag, "remote monitor thread interrupted.")
          case e => {
            Log.d(Tag, "feed exception " + e.getMessage)
            e.printStackTrace
          }
        }
        finally {
          if (feed != null) feed.cancel
        }

        // sleep 5 sec
        try {
          if (running) Thread.sleep(5 * 1000)
        }
        catch {
          case _ =>
        }
      }
      Log.i(Tag, "remote monitor thread exit")
    }
  }

  // ------------------------ debug -------------------------
  def dumpContext() = contentLock.synchronized {
    import Helper._

    val provider = Shuffle.Contexts
    withCursor(cr.query(provider.uri,
                        null, null, null, null)) { cur => {
      while (cur.moveToNext) {
        val row = cursorExtract(cur, provider.projection, provider.types)
        val item = provider.toItem(row)
        Log.d(Tag, "context dump[" + cur.getPosition + "]: " + item)
      }
    }}
  }

  // ------------------------ impl ------------------------------
  override def onCreate = {
    cr.registerContentObserver(Shuffle.Tasks.uri , true, observer)
    cr.registerContentObserver(Shuffle.Projects.uri , true, observer)
    cr.registerContentObserver(Shuffle.Contexts.uri , true, observer)

    timer = new Timer("remote service rebind", true)

    // bind couchdb conn
    if (cf.useLocal) {
      bindService(new Intent(classOf[ICouchbaseService].getName),
                  conn, Context.BIND_AUTO_CREATE)
    }
    else {
      initDb(cf.host, cf.port)
    }

    // remote change
    remoteMonitorThread.start

    // worker
    worker.start

    onLocalChange()
  }


  override def onStartCommand(i: Intent, flags: Int, startId: Int): Int = {
    Service.START_STICKY
  }

  override def onDestroy: Unit = {
    if (cf.useLocal) unbindService(conn)
    cr.unregisterContentObserver(observer)
    cr.unregisterContentObserver(observer)
    cr.unregisterContentObserver(observer)

    timer.cancel

    remoteMonitorThread.running = false
    remoteMonitorThread.interrupt
    worker.interrupt

  }
  override def onLowMemory: Unit = {}
  override def onBind(intent: Intent): IBinder = { null }
}
